using Sidvall.CodeGenerator.DataObjects;
using Sidvall.CodeGenerator.SortComparison;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Data.Entity.Core.Metadata.Edm;
using System.Linq;
using System.Xml.Linq;
using EdmxComplexType = System.Data.Entity.Core.Metadata.Edm.ComplexType;
using EdmxEdmItemCollection = System.Data.Entity.Core.Metadata.Edm.EdmItemCollection;
using EdmxEdmMember = System.Data.Entity.Core.Metadata.Edm.EdmMember;
using EdmxEdmProperty = System.Data.Entity.Core.Metadata.Edm.EdmProperty;
using EdmxEdmType = System.Data.Entity.Core.Metadata.Edm.EdmType;
using EdmxEntityContainer = System.Data.Entity.Core.Metadata.Edm.EntityContainer;
using EdmxEntitySet = System.Data.Entity.Core.Metadata.Edm.EntitySet;
using EdmxEntityType = System.Data.Entity.Core.Metadata.Edm.EntityType;
using EdmxFacet = System.Data.Entity.Core.Metadata.Edm.Facet;
using EdmxNavigationProperty = System.Data.Entity.Core.Metadata.Edm.NavigationProperty;
using EdmxRefType = System.Data.Entity.Core.Metadata.Edm.RefType;

namespace Sidvall.CodeGenerator
{
    [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling")]
    public class EdmxHelper
    {
        #region Public Members

        #region NewDomainModel

        public static DomainModel NewDomainModel(string fileName)
        {
            return NewDomainModel(fileName, null);
        }
        public static DomainModel NewDomainModel(string fileName, EdmxImportSettings settings)
        {
            EdmxHelper helper;
            DomainModel domainModel;
            DomainObject item, relatedObject;
            DomainObjectProperty domainObjectProperty;
            DomainObjectRelation domainObjectRelation;
            System.Collections.Generic.Dictionary<string, object> nameIndex;
            EntityMapping entityMapping;

            if (settings == null)
                settings = new EdmxImportSettings();
            helper = new EdmxHelper
            {
                Settings = settings,
                _filename = fileName,
                _isParsed = false,
                Containers = new List<Container>(),
            };
            helper.Parse();
            domainModel = new DomainModel();
            foreach (var container in helper.Containers)
            {
                domainModel.DataLayerContainerName = container.Name;

                foreach (var entity in container.Entities)
                {
                    entityMapping = helper.Settings.EntityMappings.GetItem(entity.Name);
                    if ((entityMapping != null) && (entityMapping.Ignore))
                        continue;
                    item = new DomainObject()
                    {
                        Name = entity.Name,
                        NameInDataStore = entity.Name,
                        IsDatabaseOnly = false,
                    };
                    foreach (var scalar in entity.Scalars)
                    {
                        domainObjectProperty = helper.NewDomainObjectProperty(scalar, entity.KeyCount);
                        item.Properties.Add(domainObjectProperty);
                    }
                    domainModel.DatabaseDomainObjects.Add(item);
                }

                foreach (var entity in container.Entities)
                {
                    try
                    {
                        nameIndex = new Dictionary<string, object>();
                        nameIndex.Add(entity.Name, null);
                        item = domainModel.DatabaseDomainObjects.FindByName(entity.Name);
                    }
                    catch (System.Exception ex)
                    {
                        throw ex;
                    }
                    foreach (var reference in entity.References)
                    {
                        try
                        {
                            relatedObject = domainModel.DatabaseDomainObjects.FindByName(reference.DestinationDataType.EntityName);
                            domainObjectRelation = helper.NewDomainObjectRelation(reference, item, relatedObject, nameIndex);
                            if (domainObjectRelation != null)
                            {
                                item.DatabaseRelations.Add(domainObjectRelation);
                                nameIndex.Add(domainObjectRelation.Name, null);
                            }
                        }
                        catch (System.Exception ex)
                        {
                            throw ex;
                        }
                    }
                }
            }

            return domainModel;
        }

        #endregion

        #endregion
        #region Private Members

        private EdmxImportSettings Settings { get; set; }
        private List<Container> Containers { get; set; }
        private Container _currentContainer;
        private EdmxEdmItemCollection _metadata;
        private EdmxEntityContainer _edmxContainer;
        private ReadOnlyCollection<EdmxComplexType> _edmxComplexTypes;
        private string _filename;
        private bool _isParsed;

        #region TypeMapper

        private TypeMapper _TypeMapper;
        private TypeMapper TypeMapper
        {
            get
            {
                if (_TypeMapper == null)
                    _TypeMapper = new TypeMapper(CodeGenerationTools, this.MetadataTools);
                return _TypeMapper;
            }
        }

        #endregion
        #region TableRelationIndex

        private System.Collections.Generic.Dictionary<string, System.Collections.Generic.List<Reference>> _TableRelationIndex;
        private System.Collections.Generic.Dictionary<string, System.Collections.Generic.List<Reference>> TableRelationIndex
        {
            get
            {
                if (_TableRelationIndex == null)
                    _TableRelationIndex = new Dictionary<string, List<Reference>>();
                return _TableRelationIndex;
            }
        }

        #endregion
        #region MetadataTools

        private MetadataTools _MetadataTools;
        private MetadataTools MetadataTools
        {
            get
            {
                if (_MetadataTools == null)
                    _MetadataTools = new MetadataTools();
                return _MetadataTools;
            }
        }

        #endregion
        #region CodeGenerationTools

        private CodeGenerationTools _CodeGenerationTools;
        private CodeGenerationTools CodeGenerationTools
        {
            get
            {
                if (_CodeGenerationTools == null)
                    _CodeGenerationTools = new CodeGenerationTools();
                return _CodeGenerationTools;
            }
        }

        #endregion

        #region NewDomainObjectProperty

        private DomainObjectProperty NewDomainObjectProperty(Scalar item, int keyCount)
        {
            var typeMapping = this.Settings.PropertyTypeMappings.GetItem(item.ScalarType.TypeName);
            var domainObjectProperty = new DomainObjectProperty()
            {
                Comment = string.Empty,
                DefaultValue = item.DefaultValue.ToString(null),
                IsComputed = item.IsComputed,
                IsComputedInDataStore = item.IsComputed,
                IsIdentityInDataStore = item.IsIdentity,
                IsValueType = item.ScalarType.IsValueType,
                IsValueTypeInDataStore = item.ScalarType.IsValueType,
                IsKey = item.IsKey,
                MaxLength = item.MaxLength,
                MaxDecimalCount = item.Scale,
                Name = item.Name,
                NameInDataStore = item.Name,
                TypeName = (typeMapping != null) ? typeMapping.NewTypeName : item.ScalarType.TypeName,
                TypeNameInDataStore = item.ScalarType.TypeName,
                ValueFormat = (typeMapping != null) ? typeMapping.ValueFormat : null,
                ValueFormatInDataStore = (typeMapping != null) ? typeMapping.ValueFormatInDataStore : null,
                ConcurrencyMode = item.ConcurrencyMode,
            };

            if ((this.Settings.UseAutoKeys) && ((domainObjectProperty.IsKeyResolved) && (keyCount == 1)))
                domainObjectProperty.IsIdentity = true;
            else
                domainObjectProperty.IsIdentity = item.IsIdentity;
            domainObjectProperty.IsNullable = item.ScalarType.IsNullable;
            domainObjectProperty.IsNullableInDataStore = item.ScalarType.IsNullable;

            return domainObjectProperty;
        }

        #endregion
        #region NewDomainObjectRelation

        private DomainObjectRelation NewDomainObjectRelation(Reference reference, DomainObject sourceObject, DomainObject relatedObject, System.Collections.Generic.Dictionary<string, object> nameIndex)
        {
            bool isDatabaseOnly;

            if ((reference.Fields.Count > 0) && (sourceObject.FullName == relatedObject.FullName))
            {
                isDatabaseOnly = true;
                foreach (var field in reference.Fields)
                {
                    if (field.DstName != field.SrcName)
                    {
                        isDatabaseOnly = false;
                        break;
                    }
                }
            }
            else
            {
                isDatabaseOnly = false;
            }
            var domainObjectRelation = new DomainObjectRelation(sourceObject, relatedObject)
            {
                SourceObjectMultiplicity = reference.SourceDataType.Multiplicity,
                RelatedObjectMultiplicity = reference.DestinationDataType.Multiplicity,
                NameInDataStore = reference.Name,
                IsDatabaseOnly = isDatabaseOnly,
                CascadeDelete = reference.CascadeDelete,
            };
            switch (domainObjectRelation.RelatedObjectMultiplicity)
            {
                case Multiplicity.Many:
                    domainObjectRelation.NameIsPlural = true;
                    break;
                default:
                    domainObjectRelation.NameIsPlural = false;
                    break;
            }
            if (this.Settings.UseAutoRelationNames)
            {
                switch (domainObjectRelation.RelatedObjectMultiplicity)
                {
                    case Multiplicity.Many:
                        domainObjectRelation.Name = TypeMapper.GetPluralName(relatedObject.Name);
                        break;
                    default:
                        domainObjectRelation.Name = TypeMapper.GetSingularName(relatedObject.Name);
                        break;
                }
                if (domainObjectRelation.Name == sourceObject.Name)
                {
                    domainObjectRelation.Name = "Parent" + domainObjectRelation.Name;
                }
                if (sourceObject.Properties.Contains(domainObjectRelation.Name))
                {
                    domainObjectRelation.Name += "Relation";
                }
                if (nameIndex.ContainsKey(domainObjectRelation.Name))
                {
                    if (reference.Fields.Count > 0)
                    {
                        domainObjectRelation.Name += "By";
                        switch (domainObjectRelation.RelatedObjectMultiplicity)
                        {
                            case Multiplicity.Many:
                                foreach (var field in reference.Fields)
                                    domainObjectRelation.Name += field.DstName;
                                break;
                            default:
                                foreach (var field in reference.Fields)
                                    domainObjectRelation.Name += field.SrcName;
                                break;
                        }
                    }
                    else
                    {
                        var count = 2;
                        while (nameIndex.ContainsKey(domainObjectRelation.Name + count.ToString()))
                        {
                            count += 1;
                        }
                        domainObjectRelation.Name += count.ToString();
                    }
                }
            }
            else
            {
                domainObjectRelation.Name = domainObjectRelation.NameInDataStore;
            }
            foreach (var field in reference.Fields)
            {
                var sourceProperty = sourceObject.Properties.FindByName(field.SrcName);
                if (sourceProperty == null)
                    return null;
                var relatedProperty = relatedObject.Properties.FindByName(field.DstName);
                if (relatedProperty == null)
                    return null;
                var domainObjectRelationField = new DomainObjectRelationField(sourceProperty, relatedProperty);
                domainObjectRelation.Fields.Add(domainObjectRelationField);
            }

            return domainObjectRelation;
        }

        #endregion

        #region CreateScalarProperty

        private void CreateScalarProperty(ICollection<EdmxEdmMember> edmxKeyMembers, Entity entity, EdmxEdmProperty edmxEdmProperty)
        {
            IEnumerable<MetadataProperty> annotations;
            Scalar property;

            try
            {
                property = new Scalar
                {
                    Name = edmxEdmProperty.Name,
                    IsKey = (edmxKeyMembers == null) ? false : edmxKeyMembers.Contains(edmxEdmProperty),
                    DefaultValue = edmxEdmProperty.DefaultValue,
                    ScalarType = GetPocoClrType(edmxEdmProperty),
                    ConcurrencyMode = GetConcurrencyMode(edmxEdmProperty),
                };
                annotations = edmxEdmProperty.MetadataProperties.Where(p => p.Name.StartsWith("http://schemas.microsoft.com", StringComparison.OrdinalIgnoreCase));
                foreach (MetadataProperty annotation in annotations)
                {
                    if (annotation.Name.EndsWith("StoreGeneratedPattern", StringComparison.OrdinalIgnoreCase))
                    {
                        switch (annotation.Value.ToString(string.Empty).ToUpperInvariant())
                        {
                            case "COMPUTED":
                                property.IsComputed = true;
                                break;
                            case "IDENTITY":
                                property.IsIdentity = true;
                                break;
                            default:
                                break;
                        }
                    }
                }

                SetConstraints(property, edmxEdmProperty);

                if (property.IsKey)
                    entity.KeyCount += 1;
                entity.Scalars.Add(property);

                if (!IsPrimitiveType(edmxEdmProperty))
                    CreateComplexType(edmxEdmProperty.TypeUsage.EdmType);
            }
            catch
            {
                throw;
            }
        }

        #endregion
        #region CreateNavigationProperties

        private void CreateNavigationProperties(EdmxEntityType edmxEntityType, Entity entity)
        {
            ReferenceField referenceField;
            List<Reference> tableReferences;

            var edmxNavigationProperties = edmxEntityType.NavigationProperties.Where(c => c.DeclaringType == edmxEntityType);
            foreach (EdmxNavigationProperty edmxNavigationProperty in edmxNavigationProperties)
            {
                try
                {
                    var srcDataType = ((EdmxRefType)edmxNavigationProperty.FromEndMember.TypeUsage.EdmType);
                    var dstDataType = ((EdmxRefType)edmxNavigationProperty.ToEndMember.TypeUsage.EdmType);

                    var reference = new Reference
                    {
                        Name = edmxNavigationProperty.Name,
                        SourceDataType = new ReferenceDataType
                        {
                            EntityName = srcDataType.ElementType.Name,
                            EntityNamespace = srcDataType.ElementType.NamespaceName,
                            TableName = edmxNavigationProperty.FromEndMember.Name,
                            Multiplicity = Convert(edmxNavigationProperty.FromEndMember.RelationshipMultiplicity)
                        },
                        DestinationDataType = new ReferenceDataType
                        {
                            EntityName = dstDataType.ElementType.Name,
                            EntityNamespace = dstDataType.ElementType.NamespaceName,
                            TableName = edmxNavigationProperty.ToEndMember.Name,
                            Multiplicity = Convert(edmxNavigationProperty.ToEndMember.RelationshipMultiplicity)
                        }
                    };
                    if (edmxNavigationProperty.FromEndMember.DeleteBehavior == OperationAction.Cascade)
                    {
                        reference.CascadeDelete = true;
                    }

                    // Fields
                    var associationType = (from o in _metadata.GetItems<AssociationType>()
                                           where (o.Name == edmxNavigationProperty.RelationshipType.Name) && (o.AssociationEndMembers != null) && (o.ReferentialConstraints.Any())
                                           select o).FirstOrDefault();
                    if (associationType != null)
                    {
                        var referentialConstraint = associationType.ReferentialConstraints.FirstOrDefault();
                        var iMax = referentialConstraint.FromProperties.Count;
                        for (int i = 0; i < iMax; i++)
                        {
                            if (referentialConstraint.FromRole == edmxNavigationProperty.FromEndMember)
                            {
                                referenceField = new ReferenceField()
                                {
                                    DstName = referentialConstraint.ToProperties[i].Name,
                                    SrcName = referentialConstraint.FromProperties[i].Name
                                };
                            }
                            else
                            {
                                referenceField = new ReferenceField()
                                {
                                    DstName = referentialConstraint.FromProperties[i].Name,
                                    SrcName = referentialConstraint.ToProperties[i].Name
                                };
                            }
                            reference.Fields.Add(referenceField);
                        }
                    }

                    if (!TableRelationIndex.TryGetValue(reference.TableKey, out tableReferences))
                    {
                        tableReferences = new List<Reference>();
                        TableRelationIndex.Add(reference.TableKey, tableReferences);
                    }
                    tableReferences.Add(reference);
                    if (tableReferences.Count > 1)
                    {
                        foreach (var tableReference in tableReferences)
                            tableReference.ExistsMultipleRelationsBetweenTables = true;
                    }
                    reference.Fields.Sort(new SortReferenceFields());

                    entity.References.Add(reference);
                }
                catch
                {
                    throw;
                }
            }
        }

        #endregion
        #region GetPocoClrType

        private DataType GetPocoClrType(EdmProperty prop)
        {
            Type type;
            EdmType edmType;

            edmType = prop.TypeUsage.EdmType;
            type = this.TypeMapper.UnderlyingClrType(edmType);

            PrimitiveType pt = edmType as PrimitiveType;
            if (pt != null)
            {
                return new DataType
                {
                    IsNullable = prop.Nullable,
                    Name = pt.ClrEquivalentType.Name,
                    Namespace = pt.ClrEquivalentType.Namespace,
                    TypeName = this.TypeMapper.GetTypeName(edmType, prop.Nullable, null),
                    IsValueType = type.IsValueType
                };
            }

            return new DataType
            {
                IsNullable = false,
                Name = edmType.Name,
                Namespace = edmType.NamespaceName,
                TypeName = this.TypeMapper.GetTypeName(edmType, prop.Nullable, null),
                IsValueType = type.IsValueType
            };
        }

        #endregion
        #region GetConcurrencyMode

        private static ConcurrencyMode GetConcurrencyMode(EdmProperty prop)
        {
            switch (prop.ConcurrencyMode)
            {
                case System.Data.Entity.Core.Metadata.Edm.ConcurrencyMode.Fixed:
                    return ConcurrencyMode.Fixed;
                default:
                    return ConcurrencyMode.None;
            }
        }

        #endregion
        #region Parse

        private void Parse()
        {
            if (_isParsed)
                return;

            _metadata = ReadCsdlCollection(_filename);
            _edmxComplexTypes = _metadata.GetItems<EdmxComplexType>();

            foreach (EdmxEntityContainer edmxContainer in _metadata.GetItems<EdmxEntityContainer>())
            {
                _edmxContainer = edmxContainer;
                _currentContainer = new Container()
                {
                    Name = edmxContainer.Name,
                };
                this.Containers.Add(_currentContainer);
                CreateEntitySets();
            }
            this.Containers.Sort(new SortContainer());

            _isParsed = true;
        }

        #endregion
        #region SetConstraints

        private static void SetConstraints(Scalar property, EdmxEdmMember edmxEdmMember)
        {
            foreach (EdmxFacet facet in edmxEdmMember.TypeUsage.Facets)
            {
                switch (facet.Name)
                {
                    case "DefaultValue":
                    case "FixedLength":
                    case "Nullable":
                    case "Unicode":
                        break;
                    case "StoreGeneratedPattern":
                        break;
                    case "MaxLength":
                        property.MaxLength = (facet.Value is int) ? (int?)facet.Value : null;
                        break;
                    case "Precision":
                        property.Precision = (facet.Value is byte) ? (byte?)facet.Value : null;
                        break;
                    case "Scale":
                        property.Scale = (facet.Value is byte) ? (byte?)facet.Value : null;
                        break;
                    default:
                        property.Facets.Add(facet.Name, facet.Value);
                        break;
                }
            }
        }

        #endregion
        #region CreateEntitySets

        private void CreateEntitySets()
        {
            var entitySets = _edmxContainer.BaseEntitySets.OfType<EdmxEntitySet>();
            foreach (EdmxEntitySet edmxEntitySet in entitySets)
            {
                var item = CreateEntityType(edmxEntitySet.ElementType, edmxEntitySet.Name);
                if (item != null)
                    _currentContainer.Entities.Add(item);
            }
        }

        #endregion
        #region CreateComplexType

        private void CreateComplexType(EdmxEdmType edmxEdmType)
        {
            if (_currentContainer.Structures.Any(s => s.Name.Equals(edmxEdmType.Name) && s.Namespace.Equals(edmxEdmType.NamespaceName)))
            {
                // already created it
                return;
            }

            EdmxComplexType edmxComplexType = _edmxComplexTypes.SingleOrDefault(c => c.FullName.Equals(edmxEdmType.FullName));
            if (edmxComplexType == null)
                throw new TypeLoadException(edmxEdmType.FullName + " was not found.");

            Entity entity = new Entity
            {
                Name = edmxComplexType.Name,
                Namespace = edmxComplexType.NamespaceName,
                BaseName = (edmxComplexType.BaseType == null) ? null : edmxComplexType.BaseType.Name,
                BaseNamespace = (edmxComplexType.BaseType == null) ? null : edmxComplexType.BaseType.NamespaceName,
                IsAbstract = edmxComplexType.Abstract
            };

            foreach (EdmxEdmProperty edmxEdmProperty in edmxComplexType.Properties.Where(c => c.DeclaringType == edmxComplexType))
                CreateScalarProperty(null, entity, edmxEdmProperty);

            _currentContainer.Structures.Add(entity);
        }

        #endregion
        #region CreateEntityType

        private Entity CreateEntityType(EdmxEntityType edmxEntityType, string setName)
        {
            var entity = new Entity
            {
                SetName = setName,
                Name = edmxEntityType.Name,
                Namespace = edmxEntityType.NamespaceName,
                BaseName = (edmxEntityType.BaseType == null) ? null : edmxEntityType.BaseType.Name,
                BaseNamespace = (edmxEntityType.BaseType == null) ? null : edmxEntityType.BaseType.NamespaceName,
                IsAbstract = edmxEntityType.Abstract
            };
            CreateScalarProperties(edmxEntityType, entity);
            CreateNavigationProperties(edmxEntityType, entity);

            entity.Scalars.Sort(new SortScalars());
            entity.References.Sort(new SortReferences());
            return entity;
        }

        #endregion

        #region CreateScalarProperties

        private void CreateScalarProperties(EdmxEntityType edmxEntityType, Entity entity)
        {
            foreach (EdmxEdmProperty edmxEdmProperty in edmxEntityType.Properties.Where(c => c.DeclaringType == edmxEntityType))
                CreateScalarProperty(edmxEntityType.KeyMembers, entity, edmxEdmProperty);
        }

        #endregion
        #region ReadCsdlCollection

        private static EdmItemCollection ReadCsdlCollection(string filename)
        {
            XElement root;

            root = XElement.Load(filename, LoadOptions.SetBaseUri | LoadOptions.SetLineInfo);
            var csdlNodes = root.Elements()
                .Where(e => e.Name.LocalName == "Runtime")
                .Elements()
                .Where(e => e.Name.LocalName == "ConceptualModels")
                .Elements();
            var readers = csdlNodes.Select(c => c.CreateReader());
            return new EdmItemCollection(readers);
        }

        #endregion
        #region IsPrimitiveType

        private static bool IsPrimitiveType(EdmProperty prop)
        {
            return (prop.TypeUsage.EdmType is PrimitiveType);
        }

        #endregion
        #region Convert

        private static Multiplicity Convert(RelationshipMultiplicity multiplicity)
        {
            switch (multiplicity)
            {
                case RelationshipMultiplicity.ZeroOrOne:
                    return Multiplicity.ZeroOrOne;
                case RelationshipMultiplicity.One:
                    return Multiplicity.One;
                case RelationshipMultiplicity.Many:
                    return Multiplicity.Many;
                default:
                    throw new ArgumentOutOfRangeException("multiplicity", multiplicity.ToString());
            }
        }

        #endregion

        #endregion
        #region Constructors

        private EdmxHelper()
        {
        }

        #endregion
    }
}
